﻿/* 
 *  Unplugged LINQ to SQL Generator
 *  
 *  Authors: José Marcenaro - josem@tercerplaneta.com
 *           Benjamín Eidelman - benjamine@tercerplaneta.com
 *  Tercer Planeta - http://www.tercerplaneta.com
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Xml.Linq;
using System.Data.Linq.Mapping;

namespace ULinq.GenTools
{
    /// <summary>
    /// This class does the actual code generation
    /// </summary>
    /// <remarks>No dependencies with the VS extensibility infrastructure, may be used from a test bench</remarks>
    public class ULinqCodeGenerator
    {
        CodeDomProvider codeProvider;
        CodeNamespace codeNS;
        XNamespace xNS;
        bool generateDataContract;
        string entityBase;

        /// <summary>
        /// Build using the specified code provider
        /// </summary>
        /// <param name="codeProvider"></param>
        public ULinqCodeGenerator(CodeDomProvider codeProvider)
        {
            this.codeProvider = codeProvider;
        }

        /// <summary>
        /// Build using the specified language
        /// </summary>
        /// <param name="language">CSharp or VB</param>
        public ULinqCodeGenerator(string language)
        {
            this.codeProvider = CodeDomProvider.CreateProvider(language);
        }
        
        /// <summary>
        /// Builds the code and returns it as a string
        /// (using the namespace indicated in the inputFileContent)
        /// </summary>
        /// <param name="inputFileName">The name (no path) of the original input file</param>
        /// <param name="inputFileContent">The entire input file contents</param>
        /// <returns>The generated code as a string</returns>
        public string BuildCode(string inputFileName, string inputFileContent)
        {
            return BuildCode(null, inputFileName, inputFileContent);
        }
        
        /// <summary>
        /// Builds the code and returns it as a string
        /// </summary>
        /// <param name="codeNamespace">Namespace to use in the generated code</param>
        /// <param name="inputFileName">The name (no path) of the original input file</param>
        /// <param name="inputFileContent">The entire input file contents</param>
        /// <returns>The generated code as a string</returns>
        public string BuildCode(string codeNamespace, string inputFileName, string inputFileContent)
        {
            string result;
            try
            {
                // build the CodeDom tree with the actual contents, place it in 'codeNS' property
                BuildCodeDom(codeNamespace, inputFileName, inputFileContent);

                // use a CodeWriter to serialize it
                using (StringWriter sw = new StringWriter())
                {
                    CodeGeneratorOptions opt = new CodeGeneratorOptions();
                    opt.BlankLinesBetweenMembers = false;
                    opt.BracingStyle = "C";
                    codeProvider.GenerateCodeFromNamespace(codeNS, sw, opt);
                    sw.Flush();
                    result = sw.ToString();
                }
            }
            catch (Exception ex)
            {
                result = ex.ToString();
            }
            return result;
        }
        
        /// <summary>
        /// Build the CodeDom tree
        /// </summary>
        /// <param name="codeNamespace">the namespace where the generated code should belong. If null, the EntityNamespace in the dbml is used</param>
        /// <param name="inputFileName">the name of the input dbml file</param>
        /// <param name="inputFileContent">the dbml file content</param>
        private void BuildCodeDom(string codeNamespace, string inputFileName, string inputFileContent)
        {

            XElement database = XElement.Parse(inputFileContent);
            xNS = database.Name.Namespace;

            // if specified, decorate every entity/property with DataContract/DataMember attributes
            generateDataContract = ((string)database.Attribute("Serialization")) == "Unidirectional";

            // if specified, used as base class for every entity on the dbml
            entityBase = (string)database.Attribute("EntityBase");

            // as we are generating only entities, use the EntityNamespace specified in the dbml
            codeNS = new CodeNamespace(((string)database.Attribute("EntityNamespace")) ?? codeNamespace);

            var tables = database.Elements(xNS + "Table");

            EmitHeader(database, tables);

            foreach (XElement table in tables)
                EmitTable(table);
        }

        /// <summary>
        /// Builds the current CodeDom CodeNamespace header,
        /// including comment and using statements
        /// </summary>
        /// <param name="database"></param>
        /// <param name="tables"></param>
        private void EmitHeader(XElement database, IEnumerable<XElement> tables)
        {
            codeNS.Comments.Add(new CodeCommentStatement(String.Format(
@"------------------------------------------------------------------------------
<auto-generated>
    This code was generated by a tool.
    Tool Name: ULinqToSQLGenerator
    Tool Version: {0}
    Runtime Version: {1}

    Changes to this file may cause incorrect behavior and will be lost if
    the code is regenerated.
</auto-generated>
------------------------------------------------------------------------------",
                System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
                System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion())));

            codeNS.Imports.Add(new CodeNamespaceImport("System"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.ComponentModel"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Collections.Generic"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Data"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Data.Linq"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Data.Linq.Mapping"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Linq"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Linq.Expressions"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Runtime.Serialization"));
        }

        /// <summary>
        /// Builds the CodeDom declaration of a Table
        /// </summary>
        /// <param name="table">a table element from a dbml file</param>
        private void EmitTable(XElement table)
        {
            string dbName = ((string)table.Attribute("Name"));
            XElement type = table.Element(xNS + "Type");
            CodeAttributeDeclaration attr;

            // table class declaration
            CodeTypeDeclaration ctd = new CodeTypeDeclaration((string)type.Attribute("Name"));
            if (entityBase != null)
                ctd.BaseTypes.Add(entityBase);
            ctd.IsPartial = true;

            // table attributes
            ctd.CustomAttributes.Add(attr = new CodeAttributeDeclaration("Table"));
            attr.Arguments.Add(new CodeAttributeArgument("Name", new CodePrimitiveExpression(dbName)));
            if (generateDataContract)
                ctd.CustomAttributes.Add(attr = new CodeAttributeDeclaration("DataContract"));

            // get columns data
            var columns = from c in type.Elements(xNS + "Column")
                          select new
                          {
                              Name = (string)c.Attribute("Name"),
                              Member = (string)(c.Attribute("Member") ?? c.Attribute("Name")),
                              Type = (string)c.Attribute("Type"),
                              DbType = (string)c.Attribute("DbType"),
                              AutoSync = (string)c.Attribute("AutoSync"),
                              IsPrimaryKey = (bool?)c.Attribute("IsPrimaryKey") ?? false,
                              IsDbGenerated = (bool?)c.Attribute("IsDbGenerated") ?? false,
                              IsVersion = (bool?)c.Attribute("IsVersion") ?? false,
                              CanBeNull = (bool?)c.Attribute("CanBeNull") ?? false,
                              UpdateCheck = (string)c.Attribute("UpdateCheck"),
                          };

            // get associations data
            var associations = from a in type.Elements(xNS + "Association")
                               select new
                               {
                                   Name = (string)a.Attribute("Name"),
                                   Type = (string)a.Attribute("Type"),
                                   Member = (string)a.Attribute("Member"),

                                   IsForeignKey = (bool?)a.Attribute("IsForeignKey") ?? false,

                                   ThisKey = (string)a.Attribute("ThisKey"),
                                   OtherKey = (string)a.Attribute("OtherKey"),
                               };



            // emit private fields for each column
            foreach (var col in columns)
            {
                // replace value types with CanBeNull parameter set with Nullable Types
                CodeTypeReference colTypeRef = new CodeTypeReference(col.Type);
                if (Type.GetType(col.Type).IsValueType && col.CanBeNull)
                    colTypeRef = new CodeTypeReference("System.Nullable", colTypeRef);


                CodeMemberField fld = new CodeMemberField(colTypeRef, "_" + col.Member);
                fld.Attributes = MemberAttributes.Private;
                ctd.Members.Add(fld);
            }

            // emit private fields for each association
            foreach (var assoc in associations)
            {
                // Set the child collection Type with Generics
                CodeTypeReference assocTypeRef = new CodeTypeReference(assoc.Type);
                if (!assoc.IsForeignKey)
                    assocTypeRef = new CodeTypeReference("List", assocTypeRef);// use List<T> by default

                CodeMemberField fld = new CodeMemberField(assocTypeRef, "_" + assoc.Member);
                fld.Attributes = MemberAttributes.Private;
                ctd.Members.Add(fld);
            }

            // emit the public property for each column
            int i = 0;
            foreach (var col in columns)
            {
                // replace value types with CanBeNull parameter set with Nullable Types
                CodeTypeReference colTypeRef = new CodeTypeReference(col.Type);
                if (Type.GetType(col.Type).IsValueType && col.CanBeNull)
                    colTypeRef = new CodeTypeReference("System.Nullable", colTypeRef);


                CodeMemberProperty prop = new System.CodeDom.CodeMemberProperty();

                // [Column ... ] attribute
                prop.CustomAttributes.Add(
                    attr = new CodeAttributeDeclaration("Column"));
                if (col.Name != col.Member)
                    attr.Arguments.Add(new CodeAttributeArgument("Name",
                        new CodePrimitiveExpression(col.Name)));
                attr.Arguments.Add(new CodeAttributeArgument("Storage",
                    new CodePrimitiveExpression("_" + col.Member)));
                attr.Arguments.Add(new CodeAttributeArgument("DbType",
                    new CodePrimitiveExpression(col.DbType)));

                if (col.AutoSync != null || col.IsDbGenerated)
                    attr.Arguments.Add(new CodeAttributeArgument("AutoSync",
                        new CodeTypeReferenceExpression("AutoSync."
                        + (col.AutoSync ?? "OnInsert"))));

                if (col.IsPrimaryKey)
                    attr.Arguments.Add(new CodeAttributeArgument("IsPrimaryKey",
                        new CodePrimitiveExpression(true)));
                if (col.IsDbGenerated || col.IsVersion)
                    attr.Arguments.Add(new CodeAttributeArgument("IsDbGenerated",
                        new CodePrimitiveExpression(true)));
                if (col.CanBeNull)
                    attr.Arguments.Add(new CodeAttributeArgument("CanBeNull",
                        new CodePrimitiveExpression(true)));

                attr.Arguments.Add(new CodeAttributeArgument("UpdateCheck",
                    new CodeTypeReferenceExpression("UpdateCheck."
                    + (col.UpdateCheck ?? "Never"))));

                // [DataMember ... ] attribute
                if (generateDataContract)
                {
                    prop.CustomAttributes.Add(attr = new CodeAttributeDeclaration("DataMember"));
                    attr.Arguments.Add(new CodeAttributeArgument("Order",
                        new CodePrimitiveExpression(i)));
                }

                // property definition
                prop.Name = col.Member;
                prop.Type = colTypeRef;
                // property get
                prop.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(null, "_" + col.Member)));
                // property set
                prop.SetStatements.Add(new CodeAssignStatement(
                    new CodeFieldReferenceExpression(null, "_" + col.Member),
                    new CodeSnippetExpression("value")));
                prop.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                ctd.Members.Add(prop);
                ++i;
            }



            // associations

            foreach (var assoc in associations)
            {

                CodeMemberProperty prop = new System.CodeDom.CodeMemberProperty();

                // [Association ... ] attribute
                prop.CustomAttributes.Add(
                    attr = new CodeAttributeDeclaration("Association"));
                attr.Arguments.Add(new CodeAttributeArgument("Name",
                    new CodePrimitiveExpression(assoc.Name)));
                attr.Arguments.Add(new CodeAttributeArgument("Storage",
                    new CodePrimitiveExpression("_" + assoc.Member)));

                if (assoc.OtherKey != null)
                {
                    attr.Arguments.Add(new CodeAttributeArgument("OtherKey",
                        new CodePrimitiveExpression(assoc.OtherKey)));
                }
                if (assoc.ThisKey != null)
                {
                    attr.Arguments.Add(new CodeAttributeArgument("ThisKey",
                        new CodePrimitiveExpression(assoc.ThisKey)));
                }

                if (assoc.IsForeignKey)
                    attr.Arguments.Add(new CodeAttributeArgument("IsForeignKey",
                        new CodePrimitiveExpression(true)));

                // [DataMember ... ] attribute
                if (generateDataContract)
                {
                    prop.CustomAttributes.Add(attr = new CodeAttributeDeclaration("DataMember"));
                    attr.Arguments.Add(new CodeAttributeArgument("Order",
                        new CodePrimitiveExpression(i)));
                }

                // Set the child collection Type with Generics
                CodeTypeReference assocTypeRef = new CodeTypeReference(assoc.Type);
                if (!assoc.IsForeignKey)
                    assocTypeRef = new CodeTypeReference("List", assocTypeRef); // use List<T> by default

                // property definition
                prop.Name = assoc.Member;
                prop.Type = assocTypeRef;
                prop.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(null, "_" + assoc.Member)));
                prop.SetStatements.Add(new CodeAssignStatement(
                    new CodeFieldReferenceExpression(null, "_" + assoc.Member),
                    new CodeSnippetExpression("value")));
                prop.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                ctd.Members.Add(prop);
                ++i;
            }
            
            codeNS.Types.Add(ctd);
        }

    }
}
